package com.lcykcl.ttgen;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

public class TimeTable {
	// List of nodes for "the best conflict time table" 
	// (It means a conflict timetable with the least number of conflict course nodes)
	private ArrayList<Node> bestConflict;
	// The number of courses required by users to generate time table for the semester
	private int requiredLen;
	private Persona persona;

	// originalsiblings=> 
	//key(String) : the name of course, value(ArrayList<Node>) : A list of nodes for all sections of the course 
	// updatedsiblings=>
	//key(String) : the name of course, value(ArrayList<Node>) : A list of nodes that are not the sibling sections of the start node
	//and that are not conflict with the time of the fixedNodes(Nodes are already selected by other timetable object)
	private HashMap<String, ArrayList<Node>> originalSiblings;
	private HashMap<String, ArrayList<Node>> updatedSiblings;

	// List of combinations of year sections or list that contains only a possible start node
	// fixedEnvironment.get(0) is the list containing the nodes that should be fixed 
	// (i.e., it shouldn't have siblings in the updatedSiblings) 
	private ArrayList<ArrayList<Node>> fixedEnvironment;

	// fixedNodes are nodes that are selected from other time table object 
	private ArrayList<Node> fixedNodes;
	// completedTImeTalbe is the timetable to be returned 
	private ArrayList<Node> completedTimeTable;
	// isTimetable is true iff a non-conflict timetable is generated
	private boolean isTimetableGernerated;

	/**
	 * A constructor for TimeTable class
	 * @param nodeList
	 *            nodeList to generate the original sibling map
	 * @param fixedNodes
	 *            fixedNodes are nodes that are selected from other time table object
	 * @param p
	 *            persona
	 */
	public TimeTable(ArrayList<Node> nodeList, ArrayList<Node> fixedNodes,
			Persona p) {

		// initiation of instance variables
		this.bestConflict = new ArrayList<Node>();
		this.requiredLen = originalSiblings.values().size();
		this.originalSiblings = new HashMap<String, ArrayList<Node>>();
		this.persona = p;
		setOriginalSibling(nodeList);
		// this.fixedEnvironment = fixedEnvironment;
		this.fixedNodes = fixedNodes;
		this.completedTimeTable = new ArrayList<Node>();
		this.isTimetableGernerated = false;
		this.updatedSiblings = (HashMap<String, ArrayList<Node>>) originalSiblings.clone();
	}


	/**
	 * uses startNode and uses generatePath returns a time table that are list
	 * of nodes for the semester. Timetable can't be conflict.
	 * @change1 If timetable is null then set the bestconflict timetalbe as completedTimeTable.
	 * @change2 generateTTB is trying to generate timetable for one start node.
	 * The reason of change2: This change is devised because I had to put yearsections and siblings of the start node together
	 * as the fixedEvironment. 
	 */

	public void generateTTB() {

		// ArrayList<ArrayList<Node>> allTimetables = new ArrayList<Node>;
		ArrayList<Node> empty = new ArrayList<Node>();

		// set value to the updatedSiblings
		setUpdatedSiblings();

		// set fixedEvironment for the updatedSiblings();
		setfixedEnvironment();
		
		// Get start Node 
		Node startNode = getStartNode();

		// set Edges for all Nodes, starting from startNode
		setEdges(startNode);
		ArrayList<Node> timetable = generatePath(startNode,startNode.getEdges(), empty);
		//If the timetable hasn't generated then get all the courses that are not examined yet
		//and put them together with the bestConflict and set it as the completedTimeTable 
		if (timetable == null) {
			Iterator<String> iter = this.originalSiblings.keySet().iterator();
			while (iter.hasNext()) {
				String courseName = iter.next();
				if (!isBestConflictContained(courseName)) {
					this.bestConflict.add(this.originalSiblings.get(courseName)
							.get(0));
				}
			}
			this.completedTimeTable = this.bestConflict;
			//else set timetable as the completedTimeTable
		} else {
			this.isTimetableGernerated = true;
			this.completedTimeTable = timetable;
		}
	}

	private boolean isBestConflictContained(String courseName) {
		for (int i = 0; i < this.bestConflict.size(); i++) {
			if (this.bestConflict.get(i).getCode() == courseName) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns the intersection of two lists of Nodes, i.e., returns all the
	 * nodes that are in common in list1 and list2. This method shouldn't change
	 * the order of the sorted edges.
	 */
	private ArrayList<Node> intersect(ArrayList<Node> prevEnNodeList,
			ArrayList<Node> startNodeEdgesList) {

		if (prevEnNodeList != startNodeEdgesList) {

			// create a new copy of startNodeEdgesList because retainAll changes
			// the Collection itself,
			// which means if we use startNodeEdgesList directly, startNode's
			// edge list will be modified.
			ArrayList<Node> startNodeEdgesClone = new ArrayList<Node>(
					startNodeEdgesList);

			startNodeEdgesClone.retainAll(prevEnNodeList);
			return startNodeEdgesClone;
		} else
			return startNodeEdgesList;
	}

	/**
	 * This method either updates the self.bestConflict while returning null, or
	 * returns a successful timetable for the startNode specified.
	 */
	private ArrayList<Node> generatePath(Node startNode,
			ArrayList<Node> prevEn, ArrayList<Node> selected) {

		ArrayList<Node> sortedEdges = startNode.sortEdgesByPersona(persona);
		ArrayList<Node> currentEn = intersect(prevEn, sortedEdges);

		// instead of creating a new list currentSelected as in the old version,
		// here we just append
		// startNode to the ArrayList selected
		selected.add(startNode);

		int i = 0;
		while (i < currentEn.size() && selected.size() < this.requiredLen) {

			ArrayList<Node> iterList = generatePath(currentEn.get(i),
					currentEn, selected);
			if (iterList != null) {
				selected = iterList;
			}
			i++;
		}

		if (selected.size() == this.requiredLen) {
			return selected;
		} else {
			setBestConflict(selected);
			return null;
		}
	}

	// getBestConflict in the old version
	// I just changed the name because what we do is actually
	// setting the BestConflict
	private void setBestConflict(ArrayList<Node> conflictList) {

		if (this.bestConflict.size() < conflictList.size()) {
			this.bestConflict = conflictList;
		}
	}

	private void setUpdatedSiblings() {
		// fix start node/ a combination of year section
		ArrayList<Node> fixedEnv = this.fixedEnvironment.get(0);
		for (int i = 0; i < fixedEnv.size(); i++) {
			fixNode(fixedEnv.get(i));
		}
		// delete some nodes whose times are conflicted with the selected nodes
		// from the other time table(s)
		Iterator<String> iter = this.updatedSiblings.keySet().iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			int sizeOfFixed = this.fixedNodes.size();
			// if the key is not the Node's code then
			ArrayList<Node> value = this.updatedSiblings.get(key);
			for (int i = 0; i < value.size(); i++) {
				Node nodeTocheck = value.get(i);
				for (int j = 0; j < sizeOfFixed; j++) {
					if (nodeTocheck.isConflict(this.fixedNodes.get(i))) {
						this.updatedSiblings.get(key).remove(i);
					}
				}
			}
		}
	}

	private void setfixedEnvironment() {
		// return a list of lists for all combinations of year sections or(exclusive) lists for a possible start node
		ArrayList<ArrayList<Node>> fixedEnv = new ArrayList<ArrayList<Node>>();
		//get all the combination of year sections
		Iterator<String> iter = originalSiblings.keySet().iterator();
		while (iter.hasNext()) {
			// get the key
			ArrayList<ArrayList<Node>> perCopyForAll = new ArrayList<ArrayList<Node>>();
			String key = (String) iter.next();
			// check if the key is Y section,
			if (key.charAt(6) == 'Y') {
				ArrayList<Node> values = originalSiblings.get(key);
				for (int i = 0; i < values.size(); i++) {
					ArrayList<ArrayList<Node>> perCopyForOne = (ArrayList<ArrayList<Node>>) fixedEnv.clone();
					for (int j = 0; j < perCopyForOne.size(); j++) {
						perCopyForOne.get(j).add(values.get(i));
					}
					perCopyForAll.addAll(perCopyForOne);
				}
				fixedEnv = perCopyForAll;
			}
		}
		//If there is no combination of year sections, use node with the least number of siblings. 
		if (fixedEnv.isEmpty()) {
			String smallestCourseName = getSmallestKey();
			ArrayList<Node> siblings = this.updatedSiblings
					.get(smallestCourseName);
			for (int i = 0; i < siblings.size(); i++) {
				ArrayList<Node> nodeArray = new ArrayList<Node>();
				nodeArray.add(siblings.get(i));
				fixedEnv.add(nodeArray);
			}
		}
		this.fixedEnvironment = fixedEnv;
	}

	/**
	 * removes all the siblings of Node toFix in updatedSiblings Hashmap.
	 * 
	 * @param toFix
	 */
	private void fixNode(Node toFix) {
		ArrayList<Node> fixed = new ArrayList<Node>();
		fixed.add(toFix);
		String key = toFix.getCode();
		this.updatedSiblings.remove(key);
		this.updatedSiblings.put(key, fixed);
	}

	/**
	 * returns the startNode, which is the first Node of the the first
	 * combination of yearSections (which is an ArrayList) if there is a Y
	 * section. If there's no Y section, then return the startNode from the
	 * updatedSiblings HashMap.
	 * 
	 * @return
	 */
	private Node getStartNode() {
		// return the Start Node based on the information from
		// the updatedSiblings
		if (this.fixedEnvironment.size() > 0) {
			return fixedEnvironment.get(0).get(0);
		} else {
			// if there are no node to generate the time table them it should
			// return null;
			return null;
		}
	}

	/**
	 * returns the String key that has the shortest length of siblings from the
	 * updatedSiblings map.
	 * 
	 * @return
	 */
	// @work
	// getSmallKey => should get the out
	private String getSmallestKey() {
		int smallest = 100;
		String smallestKey = "";
		Iterator<String> iter = this.updatedSiblings.keySet().iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			int size = this.updatedSiblings.get(key).size();
			if (smallest > size) {
				smallest = size;
				smallestKey = key;
			}
		}
		return smallestKey;
	}

	/**
	 * set edges for all Nodes, i.e. make the graph.
	 * 
	 * @param start
	 */
	private void setEdges(Node start) {
		// setEdges from the start Node
		Iterator<String> iter = this.updatedSiblings.keySet().iterator();
		ArrayList<Node> nodesToConnect = new ArrayList<Node>();
		while (iter.hasNext()) {
			String key = iter.next();
			// if the key is not the Node's code then
			if (key.equalsIgnoreCase(start.getCode())) {
				nodesToConnect.addAll(this.updatedSiblings.get(key));
			}
		}
		int numOfNodeToconnect = nodesToConnect.size();
		while (numOfNodeToconnect > 1) {
			Node nodeToConnect1 = nodesToConnect.remove(0);
			for (int i = 0; i < nodesToConnect.size(); i++) {
				Node nodeToConnect2 = nodesToConnect.get(i);
				nodeToConnect1.addEdge(nodeToConnect2);
				nodeToConnect2.addEdge(nodeToConnect1);
			}
			numOfNodeToconnect -= 1;
		}

	}

	/**
	 * removes the index 0 ArrayList from the combinations of year sections.
	 */
	public void tryAgain() {
		// TODO Auto-generated method stub
		this.isTimetableGernerated = false;
		this.fixedEnvironment.remove(0);
	}

	public ArrayList<Node> getTimeTable() {
		// TODO Auto-generated method stub
		// if it's conflict then we just return the conflict timetable.
		return this.completedTimeTable;
	}

	public boolean isConflict() {
		// TODO Auto-generated method stub
		return !isTimetableGernerated;
	}
	
	public boolean isTTBgenerated() {
		// TODO Auto-generated method stub
		return isTimetableGernerated;
	}

	public boolean isPossibletoTryAgain() {
		// TODO Auto-generated method stub
		// If there is any environment left in the fixedEnvironment
		return !this.fixedEnvironment.isEmpty();
	}

	public void deleteConflictTime(ArrayList<Node> fixed) {
		// update fixedNodes based on the information from the outside
		// of this class
		fixedNodes = fixed;
	}

	public void updatePersona(Persona p) {
		// update Persona
		this.persona = p;
	}

	/**
	 * get the list of Nodes as input from the constructor and update the
	 * sibling dict for that particular node list (e.g.FLSiblingDict,
	 * FTSiblingDict, etc.).
	 * 
	 * @param nodeList
	 */
	private void setOriginalSibling(ArrayList<Node> nodeList) {

		// parse the node list(FLlist, FTlist, etc..) to update the dictionary
		// this dictionary will be used in the timetalbe object
		int size = nodeList.size();
		for (int i = 0; i < size; i++) {
			// get the node from the list
			Node courseNode = nodeList.get(i);
			// get the course name from the node (e.g., "csc236H1")
			String courseName = courseNode.getCode();
			// if the Node Dictionary contains the course name "csc236H1"
			// that means already the value (i.e., the array list) exists
			// so just add the node to the array list
			if (this.originalSiblings.containsKey(courseName)) {
				this.originalSiblings.get(courseName).add(courseNode);
			} else {
				// Else make the entry for the course node
				ArrayList<Node> courseNodeList = new ArrayList<Node>();
				courseNodeList.add(courseNode);
				this.originalSiblings.put(courseName, courseNodeList);
			}
		}
	}

	public HashMap<String, ArrayList<Node>> getSiblingMap() {
		return this.originalSiblings;
	}

}
